/**
 * 整合自下面两个项目：
 * * cilame/v_jstools
 * * Cqxstevexw/decodeObfuscator
 */
const { parse } = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
const ivm = require('isolated-vm')
const PluginEval = require('./eval.js')

const isolate = new ivm.Isolate()
const globalContext = isolate.createContextSync()
function virtualGlobalEval(jsStr) {
  return globalContext.evalSync(String(jsStr))
}

/**
 * Extract the literal value of an object, and remove an object if all
 * references to the object are replaced.
 */
function decodeObject(ast) {
  function collectObject(path) {
    const id = path.node.id
    const init = path.node.init
    if (!t.isIdentifier(id) || !t.isObjectExpression(init)) {
      return
    }
    const obj_name = id.name
    const bind = path.scope.getBinding(obj_name)
    let valid = true
    let count = 0
    let obj = {}
    for (const item of init.properties) {
      if (!t.isObjectProperty(item) || !t.isLiteral(item.value)) {
        valid = false
        break
      }
      if (!t.isIdentifier(item.key)) {
        valid = false
        break
      }
      ++count
      obj[item.key.name] = item.value
    }
    if (!valid || !count) {
      return
    }
    let safe = true
    for (let ref of bind.referencePaths) {
      const parent = ref.parentPath
      if (ref.key !== 'object' || !parent.isMemberExpression()) {
        safe = false
        continue
      }
      const key = parent.node.property
      if (!t.isIdentifier(key) || parent.node.computed) {
        safe = false
        continue
      }
      if (Object.prototype.hasOwnProperty.call(obj, key.name)) {
        parent.replaceWith(obj[key.name])
      } else {
        safe = false
      }
    }
    bind.scope.crawl()
    if (safe) {
      path.remove()
      console.log(`删除对象: ${obj_name}`)
    }
  }
  traverse(ast, {
    VariableDeclarator: collectObject,
  })
  return ast
}

/**
 * Before version 2.19.0, the string-array is a single array.
 * Hence, we have to find StringArrayRotateFunction instead.
 *
 * @param {t.File} ast The ast file
 * @returns Object
 */
function stringArrayV2(ast) {
  console.info('Try v2 mode...')
  let obj = {
    version: 2,
    stringArrayName: null,
    stringArrayCodes: [],
    stringArrayCalls: [],
  }
  // Function to rotate string list ("func2")
  function find_rotate_function(path) {
    const callee = path.get('callee')
    const args = path.node.arguments
    if (
      !callee.isFunctionExpression() ||
      callee.node.params.length !== 2 ||
      args.length == 0 ||
      args.length > 2 ||
      !t.isIdentifier(args[0])
    ) {
      return
    }
    const arr = callee.node.params[0].name
    const cmpV = callee.node.params[1].name
    // >= 2.10.0
    const fp1 = `(){try{if()break${arr}push(${arr}shift())}catch(){${arr}push(${arr}shift())}}`
    // < 2.10.0
    const fp2 = `=function(){while(--){${arr}push(${arr}shift)}}${cmpV}`
    const code = '' + callee.get('body')
    if (!checkPattern(code, fp1) && !checkPattern(code, fp2)) {
      return
    }
    obj.stringArrayName = args[0].name
    // The string array can be found by its binding
    const bind = path.scope.getBinding(obj.stringArrayName)
    const def = t.variableDeclaration('var', [bind.path.node])
    obj.stringArrayCodes.push(generator(def, { minified: true }).code)
    // The calls can be found by its references
    for (let ref of bind.referencePaths) {
      if (ref?.listKey === 'arguments') {
        // This is the rotate function
        continue
      }
      if (ref.findParent((path) => path.removed)) {
        continue
      }
      // the key is 'object'
      let up1 = ref.getFunctionParent()
      if (up1.node.id) {
        // 2.12.0 <= v < 2.15.4
        // The `stringArrayCallsWrapperName` is included in the definition
        obj.stringArrayCalls.push(up1.node.id.name)
        obj.stringArrayCodes.push(generator(up1.node, { minified: true }).code)
        up1.remove()
        continue
      }
      if (up1.key === 'init') {
        // v < 2.12.0
        // The `stringArrayCallsWrapperName` is defined by VariableDeclarator
        up1 = up1.parentPath
        obj.stringArrayCalls.push(up1.node.id.name)
        up1 = up1.parentPath
        obj.stringArrayCodes.push(generator(up1.node, { minified: true }).code)
        up1.remove()
        continue
      }
      // 2.15.4 <= v < 2.19.0
      // The function includes another function with the same name
      up1 = up1.parentPath
      const wrapper = up1.node.left.name
      let up2 = up1.getFunctionParent()
      if (!up2 || up2.node?.id?.name !== wrapper) {
        console.warn('Unexpected reference!')
        continue
      }
      obj.stringArrayCalls.push(wrapper)
      obj.stringArrayCodes.push(generator(up2.node, { minified: true }).code)
      up2.remove()
    }
    // Remove the string array
    bind.path.remove()
    // Add the rotate function
    const node = t.expressionStatement(path.node)
    obj.stringArrayCodes.push(generator(node, { minified: true }).code)
    path.stop()
    if (path.parentPath.isUnaryExpression()) {
      path.parentPath.remove()
    } else {
      path.remove()
    }
  }
  traverse(ast, { CallExpression: find_rotate_function })
  if (obj.stringArrayCodes.length < 3 || !obj.stringArrayCalls.length) {
    console.error('Essential code missing!')
    obj.stringArrayName = null
  }
  return obj
}

/**
 * Find the string-array codes by matching string-array function
 * (valid version >= 2.19.0)
 *
 * @param {t.File} ast The ast file
 * @returns Object
 */
function stringArrayV3(ast) {
  console.info('Try v3 mode...')
  let ob_func_str = []
  let ob_dec_name = []
  let ob_string_func_name = null
  // Normally, the string array func ("func1") follows the template below:
  // function aaa() {
  //   const bbb = [...]
  //   aaa = function () {
  //     return bbb;
  //   };
  //   return aaa();
  // }
  // In some cases (lint), the assignment is merged into the ReturnStatement
  // After finding the possible func1, this method will check all the binding
  // references and put the child encode function into list.
  function find_string_array_function(path) {
    if (path.getFunctionParent()) {
      return
    }
    if (
      !t.isIdentifier(path.node.id) ||
      path.node.params.length ||
      !t.isBlockStatement(path.node.body)
    ) {
      return
    }
    const body = path.node.body.body
    if (body.length < 2 || body.length > 3) {
      return
    }
    const name_func = path.node.id.name
    let string_var = -1
    try {
      if (
        body[0].declarations.length != 1 ||
        !(string_var = body[0].declarations[0].id.name) ||
        !t.isArrayExpression(body[0].declarations[0].init)
      ) {
        return
      }
      const nodes = [...body]
      nodes.shift()
      const code = generator(t.BlockStatement(nodes)).code
      const fp = `${name_func}=function(){return${string_var}}${name_func}()`
      if (!checkPattern(code, fp)) {
        return
      }
    } catch {
      return
    }
    const binding = path.scope.getBinding(name_func)
    if (!binding.referencePaths) {
      return
    }
    let paths = binding.referencePaths
    let nodes = []
    // The sorting function maybe missing in some config
    function find2(refer_path) {
      if (
        refer_path.parentPath.isCallExpression() &&
        refer_path.listKey === 'arguments' &&
        refer_path.key === 0
      ) {
        let rm_path = refer_path.parentPath
        if (rm_path.parentPath.isExpressionStatement()) {
          rm_path = rm_path.parentPath
        }
        nodes.push([rm_path.node, 'func2'])
        rm_path.remove()
      }
    }
    paths.map(find2)
    function find3(refer_path) {
      if (refer_path.findParent((path) => path.removed)) {
        return
      }
      if (
        refer_path.parentPath.isCallExpression() &&
        refer_path.key === 'callee'
      ) {
        let rm_path = refer_path.parentPath.getFunctionParent()
        if (name_func == rm_path.node.id.name) {
          return
        }
        nodes.push([rm_path.node, 'func3'])
        rm_path.remove()
      } else {
        console.error('Unexpected reference')
      }
    }
    paths.map(find3)
    if (!name_func) {
      return
    }
    ob_string_func_name = name_func
    ob_func_str.push(generator(path.node, { minified: true }).code)
    nodes.map(function (item) {
      let node = item[0]
      if (item[1] == 'func3') {
        ob_dec_name.push(node.id.name)
      }
      if (t.isCallExpression(node)) {
        node = t.expressionStatement(node)
      }
      ob_func_str.push(generator(node, { minified: true }).code)
    })
    path.stop()
    path.remove()
  }
  traverse(ast, { FunctionDeclaration: find_string_array_function })
  return {
    version: 3,
    stringArrayName: ob_string_func_name,
    stringArrayCodes: ob_func_str,
    stringArrayCalls: ob_dec_name,
  }
}

function decodeGlobal(ast) {
  let obj = stringArrayV3(ast)
  if (!obj.stringArrayName) {
    obj = stringArrayV2(ast)
    if (!obj.stringArrayName) {
      console.error('Cannot find string list!')
      return false
    }
  }
  console.log(`String List Name: ${obj.stringArrayName}`)
  let ob_func_str = obj.stringArrayCodes
  let ob_dec_name = obj.stringArrayCalls
  try {
    virtualGlobalEval(ob_func_str.join(';'))
  } catch (e) {
    // issue #31
    if (e.name === 'ReferenceError') {
      let lost = e.message.split(' ')[0]
      traverse(ast, {
        Program(path) {
          ob_dec_name.push(lost)
          let loc = path.scope.getBinding(lost).path
          let obj = t.variableDeclaration(loc.parent.kind, [loc.node])
          ob_func_str.unshift(generator(obj, { minified: true }).code)
          loc.remove()
          path.stop()
        },
      })
      virtualGlobalEval(ob_func_str.join(';'))
    }
  }

  // 循环删除混淆函数
  let call_dict = {}
  let exist_names = ob_dec_name
  let collect_codes = []
  let collect_names = []
  function do_parse_value(path) {
    let name = path.node.callee.name
    if (path.node.callee && exist_names.indexOf(name) != -1) {
      let old_call = path + ''
      try {
        // 运行成功则说明函数为直接调用并返回字符串
        let new_str = virtualGlobalEval(old_call)
        console.log(`map: ${old_call} -> ${new_str}`)
        call_dict[old_call] = new_str
      } catch (e) {
        // 运行失败则说明函数为其它混淆函数的子函数
        console.log(`sub: ${old_call}`)
      }
    }
  }
  function do_collect_remove(path) {
    // 可以删除所有已收集混淆函数的定义
    // 因为根函数已被删除 即使保留也无法运行
    let node = path.node?.left
    if (!node) {
      node = path.node?.id
    }
    let name = node?.name
    if (exist_names.indexOf(name) != -1) {
      // console.log(`del: ${name}`)
      if (path.parentPath.isCallExpression()) {
        path.replaceWith(node)
      } else {
        path.remove()
      }
    }
  }
  function do_collect_func_dec(path) {
    // function A (...) { return function B (...) }
    do_collect_func(path, path)
  }
  function do_collect_func_var(path) {
    // var A = function (...) { return function B (...) }
    let func_path = path.get('init')
    if (!func_path.isFunctionExpression()) {
      return
    }
    do_collect_func(path, func_path)
  }
  function do_collect_func(root, path) {
    if (
      path.node.body.body.length == 1 &&
      path.node.body.body[0].type == 'ReturnStatement' &&
      path.node.body.body[0].argument?.type == 'CallExpression' &&
      path.node.body.body[0].argument.callee.type == 'Identifier' &&
      // path.node.params.length == 5 &&
      root.node.id
    ) {
      let call_func = path.node.body.body[0].argument.callee.name
      if (exist_names.indexOf(call_func) == -1) {
        return
      }
      let name = root.node.id.name
      let t = generator(root.node, { minified: true }).code
      if (collect_names.indexOf(name) == -1) {
        collect_codes.push(t)
        collect_names.push(name)
      } else {
        console.log(`err: redef ${name}`)
      }
    }
  }
  function do_collect_var(path) {
    // var A = B
    let left, right
    if (t.isVariableDeclarator(path.node)) {
      left = path.node.id
      right = path.node.init
    } else {
      left = path.node.left
      right = path.node.right
    }
    if (right?.type == 'Identifier' && exist_names.indexOf(right.name) != -1) {
      let name = left.name
      let t = 'var ' + generator(path.node, { minified: true }).code
      if (collect_names.indexOf(name) == -1) {
        collect_codes.push(t)
        collect_names.push(name)
      } else {
        console.warning(`redef ${name}`)
      }
    }
  }
  while (exist_names.length) {
    // 查找已收集混淆函数的调用并建立替换关系
    traverse(ast, { CallExpression: do_parse_value })
    // 删除被使用过的定义
    traverse(ast, { FunctionDeclaration: do_collect_remove })
    traverse(ast, { VariableDeclarator: do_collect_remove })
    traverse(ast, { AssignmentExpression: do_collect_remove })
    // 收集所有调用已收集混淆函数的混淆函数
    collect_codes = []
    collect_names = []
    traverse(ast, { FunctionDeclaration: do_collect_func_dec })
    traverse(ast, { VariableDeclarator: do_collect_func_var })
    traverse(ast, { VariableDeclarator: do_collect_var })
    traverse(ast, { AssignmentExpression: do_collect_var })
    exist_names = collect_names
    // 执行找到的函数
    virtualGlobalEval(collect_codes.join(';'))
  }
  // 替换混淆函数
  function do_replace(path) {
    let old_call = path + ''
    if (Object.prototype.hasOwnProperty.call(call_dict, old_call)) {
      path.replaceWith(t.StringLiteral(call_dict[old_call]))
    }
  }
  traverse(ast, { CallExpression: do_replace })
  return true
}

function stringArrayLite(ast) {
  const visitor = {
    VariableDeclarator(path) {
      const name = path.node.id.name
      if (!path.get('init').isArrayExpression()) {
        return
      }
      const elements = path.node.init.elements
      for (const element of elements) {
        if (!t.isLiteral(element)) {
          return
        }
      }
      const bind = path.scope.getBinding(name)
      if (!bind.constant) {
        return
      }
      for (const ref of bind.referencePaths) {
        if (
          !ref.parentPath.isMemberExpression() ||
          ref.key !== 'object' ||
          ref.parentPath.key == 'left' ||
          !t.isNumericLiteral(ref.parent.property)
        ) {
          return
        }
      }
      console.log(`Extract string array: ${name}`)
      for (const ref of bind.referencePaths) {
        const i = ref.parent.property.value
        ref.parentPath.replaceWith(elements[i])
      }
      bind.scope.crawl()
      path.remove()
    },
  }
  traverse(ast, visitor)
}

function calcBinary(path) {
  let tps = ['StringLiteral', 'BooleanLiteral', 'NumericLiteral']
  let nod = path.node
  function judge(e) {
    return (
      tps.indexOf(e.type) != -1 ||
      (e.type == 'UnaryExpression' && tps.indexOf(e.argument.type) != -1)
    )
  }
  function make_rep(e) {
    if (typeof e == 'number') {
      return t.NumericLiteral(e)
    }
    if (typeof e == 'string') {
      return t.StringLiteral(e)
    }
    if (typeof e == 'boolean') {
      return t.BooleanLiteral(e)
    }
    throw Error('unknown type' + typeof e)
  }
  if (judge(nod.left) && judge(nod.right)) {
    path.replaceWith(make_rep(eval(path + '')))
  }
}

function decodeCodeBlock(ast) {
  // 合并字面量
  traverse(ast, { BinaryExpression: { exit: calcBinary } })
  // 先合并分离的Object定义
  const mergeObject = require('../visitor/merge-object')
  traverse(ast, mergeObject)
  // 在变量定义完成后判断是否为代码块加密内容
  const parseControlFlowStorage = require('../visitor/parse-control-flow-storage')
  traverse(ast, parseControlFlowStorage)
  // 合并字面量(在解除区域混淆后会出现新的可合并分割)
  traverse(ast, { BinaryExpression: { exit: calcBinary } })
  return ast
}

function purifyBoolean(path) {
  // 简化 ![] 和 !![]
  const node0 = path.node
  if (node0.operator !== '!') {
    return
  }
  const node1 = node0.argument
  if (t.isArrayExpression(node1) && node1.elements.length === 0) {
    path.replaceWith(t.booleanLiteral(false))
    return
  }
  if (!t.isUnaryExpression(node1) || node1.operator !== '!') {
    return
  }
  const node2 = node1.argument
  if (t.isArrayExpression(node2) && node2.elements.length === 0) {
    path.replaceWith(t.booleanLiteral(true))
  }
}

function cleanIFCode(path) {
  function clear(path, toggle) {
    // 判定成立
    if (toggle) {
      if (path.node.consequent.type == 'BlockStatement') {
        path.replaceWithMultiple(path.node.consequent.body)
      } else {
        path.replaceWith(path.node.consequent)
      }
      return
    }
    // 判定不成立
    if (!path.node.alternate) {
      path.remove()
      return
    }
    if (path.node.alternate.type == 'BlockStatement') {
      path.replaceWithMultiple(path.node.alternate.body)
    } else {
      path.replaceWith(path.node.alternate)
    }
  }
  // 判断判定是否恒定
  const test = path.node.test
  const types = ['StringLiteral', 'NumericLiteral', 'BooleanLiteral']
  if (test.type === 'BinaryExpression') {
    if (
      types.indexOf(test.left.type) !== -1 &&
      types.indexOf(test.right.type) !== -1
    ) {
      const left = JSON.stringify(test.left.value)
      const right = JSON.stringify(test.right.value)
      clear(path, eval(left + test.operator + right))
    }
  } else if (types.indexOf(test.type) !== -1) {
    clear(path, eval(JSON.stringify(test.value)))
  }
}

function cleanSwitchCode(path) {
  // 扁平控制：
  // 会使用一个恒为true的while语句包裹一个switch语句
  // switch语句的执行顺序又while语句上方的字符串决定
  // 首先碰断是否符合这种情况
  const node = path.node
  let valid = false
  if (t.isBooleanLiteral(node.test) && node.test.value) {
    valid = true
  }
  if (t.isArrayExpression(node.test) && node.test.elements.length === 0) {
    valid = true
  }
  if (!valid) {
    return
  }
  if (!t.isBlockStatement(node.body)) {
    return
  }
  const body = node.body.body
  if (
    !t.isSwitchStatement(body[0]) ||
    !t.isMemberExpression(body[0].discriminant) ||
    !t.isBreakStatement(body[1])
  ) {
    return
  }
  // switch语句的两个变量
  const swithStm = body[0]
  const arrName = swithStm.discriminant.object.name
  const argName = swithStm.discriminant.property.argument.name
  // 在while上面的节点寻找这两个变量
  let arr = []
  let rm = []
  path.getAllPrevSiblings().forEach((pre_path) => {
    if (!pre_path.isVariableDeclaration()) {
      return
    }
    for (let i = 0; i < pre_path.node.declarations.length; ++i) {
      const declaration = pre_path.get(`declarations.${i}`)
      let { id, init } = declaration.node
      if (arrName == id.name) {
        if (t.isStringLiteral(init?.callee?.object)) {
          arr = init.callee.object.value.split('|')
          rm.push(declaration)
        }
      }
      if (argName == id.name) {
        if (t.isLiteral(init)) {
          rm.push(declaration)
        }
      }
    }
  })
  if (rm.length !== 2) {
    return
  }
  rm.forEach((pre_path) => {
    pre_path.remove()
  })
  console.log(`扁平化还原: ${arrName}[${argName}]`)
  // 重建代码块
  const caseList = swithStm.cases
  let resultBody = []
  arr.map((targetIdx) => {
    // 从当前序号开始直到遇到continue
    let valid = true
    targetIdx = parseInt(targetIdx)
    while (valid && targetIdx < caseList.length) {
      const targetBody = caseList[targetIdx].consequent
      const test = caseList[targetIdx].test
      if (!t.isStringLiteral(test) || parseInt(test.value) !== targetIdx) {
        console.log(`switch中出现乱序的序号: ${test.value}:${targetIdx}`)
      }
      for (let i = 0; i < targetBody.length; ++i) {
        const s = targetBody[i]
        if (t.isContinueStatement(s)) {
          valid = false
          break
        }
        if (t.isReturnStatement(s)) {
          valid = false
          resultBody.push(s)
          break
        }
        if (t.isBreakStatement(s)) {
          console.log(`switch中出现意外的break: ${arrName}[${argName}]`)
        } else {
          resultBody.push(s)
        }
      }
      targetIdx++
    }
  })
  // 替换整个while语句
  path.replaceInline(resultBody)
}

function cleanDeadCode(ast) {
  traverse(ast, { UnaryExpression: purifyBoolean })
  traverse(ast, { IfStatement: cleanIFCode })
  traverse(ast, { ConditionalExpression: cleanIFCode })
  traverse(ast, { WhileStatement: { exit: cleanSwitchCode } })
  return ast
}

const splitVariableDeclarator = {
  VariableDeclarator(path) {
    const init = path.get('init')
    if (!init.isAssignmentExpression()) {
      return
    }
    path.parentPath.insertBefore(init.node)
    init.replaceWith(init.node.left)
    path.parentPath.scope.crawl()
  },
}

function standardIfStatement(path) {
  const consequent = path.get('consequent')
  const alternate = path.get('alternate')
  const test = path.get('test')
  const evaluateTest = test.evaluateTruthy()

  if (!consequent.isBlockStatement()) {
    consequent.replaceWith(t.BlockStatement([consequent.node]))
  }
  if (alternate.node !== null && !alternate.isBlockStatement()) {
    alternate.replaceWith(t.BlockStatement([alternate.node]))
  }

  if (consequent.node.body.length == 0) {
    if (alternate.node == null) {
      path.replaceWith(test.node)
    } else {
      consequent.replaceWith(alternate.node)
      alternate.remove()
      path.node.alternate = null
      test.replaceWith(t.unaryExpression('!', test.node, true))
    }
  }

  if (alternate.isBlockStatement() && alternate.node.body.length == 0) {
    alternate.remove()
    path.node.alternate = null
  }

  if (evaluateTest === true) {
    path.replaceWithMultiple(consequent.node.body)
  } else if (evaluateTest === false) {
    alternate.node === null
      ? path.remove()
      : path.replaceWithMultiple(alternate.node.body)
  }
}

function standardLoop(path) {
  const node = path.node
  if (!t.isBlockStatement(node.body)) {
    node.body = t.BlockStatement([node.body])
  }
}

function splitSequence(path) {
  let { scope, parentPath, node } = path
  let expressions = node.expressions
  if (parentPath.isReturnStatement({ argument: node })) {
    let lastExpression = expressions.pop()
    for (let expression of expressions) {
      parentPath.insertBefore(t.ExpressionStatement(expression))
    }

    path.replaceInline(lastExpression)
  } else if (parentPath.isExpressionStatement({ expression: node })) {
    let body = []
    expressions.forEach((express) => {
      body.push(t.ExpressionStatement(express))
    })
    path.replaceInline(body)
  } else {
    return
  }

  scope.crawl()
}

function purifyCode(ast) {
  // 标准化if语句
  traverse(ast, { IfStatement: standardIfStatement })
  // 标准化for语句
  traverse(ast, { ForStatement: standardLoop })
  // 标准化while语句
  traverse(ast, { WhileStatement: standardLoop })
  // 删除空语句
  traverse(ast, {
    EmptyStatement: (path) => {
      path.remove()
    },
  })
  // 删除未使用的变量
  traverse(ast, splitVariableDeclarator)
  const deleteUnusedVar = require('../visitor/delete-unused-var')
  traverse(ast, deleteUnusedVar)
  // 替换索引器
  function FormatMember(path) {
    let curNode = path.node
    if (!t.isStringLiteral(curNode.property)) {
      return
    }
    if (curNode.computed === undefined || !curNode.computed === true) {
      return
    }
    if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(curNode.property.value)) {
      return
    }
    curNode.property = t.identifier(curNode.property.value)
    curNode.computed = false
  }
  traverse(ast, { MemberExpression: FormatMember })

  // 替换类和对象的计算方法和计算属性
  // ["method"](){} -> "method"(){}
  function FormatComputed(path) {
    let curNode = path.node
    if (!t.isStringLiteral(curNode.key)) {
      return
    }
    curNode.computed = false
  }
  // "method"(){} -> method(){}
  function stringLiteralToIdentifier(path) {
    let curNode = path.node
    if (!t.isStringLiteral(curNode.key) || curNode.computed === true) {
      return
    }
    if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(curNode.key.value)) {
      return
    }
    curNode.key = t.identifier(curNode.key.value)
  }
  traverse(ast, {
    'Method|Property': (path) => {
      FormatComputed(path)
      stringLiteralToIdentifier(path)
    },
  })

  // 拆分语句
  traverse(ast, { SequenceExpression: splitSequence })
  return ast
}

function checkPattern(code, pattern) {
  let i = 0
  let j = 0
  while (i < code.length && j < pattern.length) {
    if (code[i] == pattern[j]) {
      ++j
    }
    ++i
  }
  return j == pattern.length
}

const deleteSelfDefendingCode = {
  VariableDeclarator(path) {
    const { id, init } = path.node
    const selfName = id.name
    if (!t.isCallExpression(init)) {
      return
    }
    if (!t.isIdentifier(init.callee)) {
      return
    }
    const callName = init.callee.name
    const args = init.arguments
    if (
      args.length != 2 ||
      !t.isThisExpression(args[0]) ||
      !t.isFunctionExpression(args[1])
    ) {
      return
    }
    const block = generator(args[1]).code
    const patterns = [
      // @7920538
      `return${selfName}.toString().search().toString().constructor(${selfName}).search()`,
      // @7135b09
      `const=function(){const=.constructor()return.test(${selfName})}return()`,
      // #94
      `var=function(){var=.constructor()return.test(${selfName})}return()`,
    ]
    let valid = false
    for (let pattern of patterns) {
      valid |= checkPattern(block, pattern)
    }
    if (!valid) {
      return
    }
    const refs = path.scope.bindings[selfName].referencePaths
    for (let ref of refs) {
      if (ref.key == 'callee') {
        ref.parentPath.remove()
        break
      }
    }
    path.remove()
    console.info(`Remove SelfDefendingFunc: ${selfName}`)
    const scope = path.scope.getBinding(callName).scope
    scope.crawl()
    const bind = scope.bindings[callName]
    if (bind.referenced) {
      console.error(`Call func ${callName} unexpected ref!`)
    }
    bind.path.remove()
    console.info(`Remove CallFunc: ${callName}`)
  },
}

const deleteDebugProtectionCode = {
  FunctionDeclaration(path) {
    const { id, params, body } = path.node
    if (
      !t.isIdentifier(id) ||
      params.length !== 1 ||
      !t.isIdentifier(params[0]) ||
      !t.isBlockStatement(body) ||
      body.body.length !== 2 ||
      !t.isFunctionDeclaration(body.body[0]) ||
      !t.isTryStatement(body.body[1])
    ) {
      return
    }
    const debugName = id.name
    const ret = params[0].name
    const subNode = body.body[0]
    if (
      !t.isIdentifier(subNode.id) ||
      subNode.params.length !== 1 ||
      !t.isIdentifier(subNode.params[0])
    ) {
      return
    }
    const subName = subNode.id.name
    const counter = subNode.params[0].name
    const code = generator(body).code
    const pattern = `function${subName}(${counter}){${counter}debugger${subName}(++${counter})}try{if(${ret})return${subName}${subName}(0)}catch(){}`
    if (!checkPattern(code, pattern)) {
      return
    }
    const scope1 = path.parentPath.scope
    const refs = scope1.bindings[debugName].referencePaths
    for (let ref of refs) {
      if (ref.findParent((path) => path.removed)) {
        continue
      }
      if (ref.key == 0) {
        // DebugProtectionFunctionInterval @e8e92c6
        const rm = ref.getFunctionParent().parentPath
        rm.remove()
        continue
      }
      // ref.key == 'callee'
      const up1 = ref.getFunctionParent()
      const callName = up1.parent.callee.name
      if (callName === 'setInterval') {
        // DebugProtectionFunctionInterval @51523c0
        const rm = up1.parentPath
        rm.remove()
        continue
      }
      const up2 = up1.getFunctionParent()?.parentPath
      if (up2) {
        // DebugProtectionFunctionCall
        const scope2 = up2.scope.getBinding(callName).scope
        up2.remove()
        scope1.crawl()
        scope2.crawl()
        const bind = scope2.bindings[callName]
        bind.path.remove()
        console.info(`Remove CallFunc: ${callName}`)
        continue
      }
      // exceptions #95
      const rm = ref.parentPath
      rm.remove()
    }
    path.remove()
    console.info(`Remove DebugProtectionFunc: ${debugName}`)
  },
}

const deleteConsoleOutputCode = {
  VariableDeclarator(path) {
    const { id, init } = path.node
    const selfName = id.name
    if (!t.isCallExpression(init)) {
      return
    }
    if (!t.isIdentifier(init.callee)) {
      return
    }
    const callName = init.callee.name
    const args = init.arguments
    if (
      args.length != 2 ||
      !t.isThisExpression(args[0]) ||
      !t.isFunctionExpression(args[1])
    ) {
      return
    }
    const block = generator(args[1]).code
    const pattern = `console=console=log,warn,info,error,for(){${callName}constructor.prototype.bind${callName}${callName}bind${callName}}`
    if (!checkPattern(block, pattern)) {
      return
    }
    const refs = path.scope.bindings[selfName].referencePaths
    for (let ref of refs) {
      if (ref.key == 'callee') {
        ref.parentPath.remove()
        break
      }
    }
    path.remove()
    console.info(`Remove ConsoleOutputFunc: ${selfName}`)
    const scope = path.scope.getBinding(callName).scope
    scope.crawl()
    const bind = scope.bindings[callName]
    if (bind.referenced) {
      console.error(`Call func ${callName} unexpected ref!`)
    }
    bind.path.remove()
    console.info(`Remove CallFunc: ${callName}`)
  },
}

function unlockEnv(ast) {
  //可能会误删一些代码，可屏蔽
  traverse(ast, deleteSelfDefendingCode)
  traverse(ast, deleteDebugProtectionCode)
  traverse(ast, deleteConsoleOutputCode)
  return ast
}

module.exports = function (code) {
  let ret = PluginEval.unpack(code)
  let global_eval = false
  if (ret) {
    global_eval = true
    code = ret
  }
  let ast
  try {
    ast = parse(code, { errorRecovery: true })
  } catch (e) {
    console.error(`Cannot parse code: ${e.reasonCode}`)
    return null
  }
  // IllegalReturn
  const deleteIllegalReturn = require('../visitor/delete-illegal-return')
  traverse(ast, deleteIllegalReturn)
  // 清理二进制显示内容
  traverse(ast, {
    StringLiteral: ({ node }) => {
      delete node.extra
    },
    NumericLiteral: ({ node }) => {
      delete node.extra
    },
  })
  console.log('还原数值...')
  if (!decodeObject(ast)) {
    return null
  }
  console.log('处理全局加密...')
  if (!decodeGlobal(ast)) {
    return null
  }
  console.log('提高代码可读性...')
  ast = purifyCode(ast)
  console.log('处理代码块加密...')
  stringArrayLite(ast)
  ast = decodeCodeBlock(ast)
  console.log('清理死代码...')
  ast = cleanDeadCode(ast)
  // 刷新代码
  ast = parse(
    generator(ast, {
      comments: false,
      jsescOption: { minimal: true },
    }).code,
    { errorRecovery: true }
  )
  console.log('提高代码可读性...')
  ast = purifyCode(ast)
  console.log('解除环境限制...')
  ast = unlockEnv(ast)
  console.log('净化完成')
  code = generator(ast, {
    comments: false,
    jsescOption: { minimal: true },
  }).code
  if (global_eval) {
    code = PluginEval.pack(code)
  }
  return code
}
